﻿using System.IO;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;

public static class XEditorAvatarBuilder
{
    public static List<string> GetAssetPathInFolder(string folderPath, bool isAllDir = true, string fileFix = "*.prefab")
    {
        string fullPath = Path.Combine(Application.dataPath, folderPath);
        string[] names = Directory.GetFiles(fullPath, fileFix,
            isAllDir ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);

        List<string> fileNames = new List<string>();
        int discardeIndex = Application.dataPath.Length + 1;
        if (names != null && names.Length > 0)
        {
            for (int i = 0; i < names.Length; i++)
            {
                int length = names[i].Length - discardeIndex;
                names[i] = names[i].Substring(discardeIndex, length);
                names[i] = names[i].Replace("\\", "/");

                fileNames.Add(Path.Combine(XDirectory.Assets.ToString(), names[i]));
            }
        }
        return fileNames;
    }

    public static Object GeneratePrefab(GameObject go, string path, string name)
    {
        string fullpath = path + "/" + name + ".prefab";

        GameObject prefab = PrefabUtility.SaveAsPrefabAsset(go, fullpath);
        GameObject.DestroyImmediate(go);

        return prefab;
    }

    public static void GenerateAsset(string path, Object asset)
    {
        AssetDatabase.DeleteAsset(path);
        AssetDatabase.CreateAsset(asset, path);
    }

    public static Object ExtractSkeleton(GameObject prefab, GameObject srcfbx, string path, AnimatorController ac)
    {
        GameObject prefabObject = GameObject.Instantiate(prefab);
        if (!prefabObject)
            throw new System.NullReferenceException(prefab.name);

        prefabObject.name = srcfbx.name;

        SkinnedMeshRenderer[] prefabRenders = prefabObject.GetComponentsInChildren<SkinnedMeshRenderer>(true);
        for (int i = 0; i < prefabRenders.Length; i++)
        {
            GameObject.DestroyImmediate(prefabRenders[i].gameObject);
        }

        Animator animator = prefabObject.GetComponent<Animator>();
        if (!animator)
            animator = prefabObject.AddComponent<Animator>();

        string outPath = Path.GetDirectoryName(path).Replace(XDirectory.Art.ToString(), XDirectory.Resources.ToString());
        Debug.LogError(outPath);

        if (Directory.Exists(outPath))
            Directory.Delete(outPath, true);

        Directory.CreateDirectory(outPath);

        if (ac)
        {
            GenerateController(prefabObject, srcfbx, path, ac, outPath);
        }

        XHumanComponent human = prefabObject.AddComponent<XHumanComponent>();
        human.GenerateBoneNogPoint();

        Rigidbody body = prefabObject.GetComponent<Rigidbody>();
        if (!body)
        {
            body = prefabObject.AddComponent<Rigidbody>();
            body.isKinematic = true;
            body.useGravity = false;
            body.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
        }

        return GeneratePrefab(prefabObject, outPath, prefab.name);
    }

    public static void GenerateController(GameObject prefab, GameObject srcfbx, string path, AnimatorController ac, string outDir)
    {
        if (ac)
        {
            List<string> eventEffects = new List<string>();

            Animator animator = prefab.GetComponent<Animator>();
            if (!animator)
                animator = prefab.AddComponent<Animator>();

            string outPath = Path.Combine(outDir, typeof(AnimatorOverrideController).Name.ToLower());
            if (Directory.Exists(outPath))
                Directory.Delete(outPath);

            Directory.CreateDirectory(outPath);

            string animationPath = Path.Combine(Path.GetDirectoryName(path),
                XDirectory.Animations.ToString()).Replace("\\", "/");

            DirectoryInfo dir = new DirectoryInfo(Path.Combine(Application.dataPath, animationPath.Replace(XDirectory.Assets.ToString() + "/", "")));
            foreach (FileSystemInfo sub in dir.GetFileSystemInfos())
            {
                if (sub is DirectoryInfo)
                {
                    string name = Path.GetFileName(sub.FullName);

                    List<string> files = GetAssetPathInFolder(sub.FullName, true, "*.fbx");
                    if (files.Count > 0)
                    {
                        AnimatorOverrideController oac = new AnimatorOverrideController(ac);
                        if (!oac)
                            throw new System.NullReferenceException();

                        List<KeyValuePair<AnimationClip, AnimationClip>> oacClips = new List<KeyValuePair<AnimationClip, AnimationClip>>(oac.overridesCount);
                        List<KeyValuePair<AnimationClip, AnimationClip>> newClips = new List<KeyValuePair<AnimationClip, AnimationClip>>(oac.overridesCount);

                        oac.GetOverrides(oacClips);

                        for (int i = 0; i < oacClips.Count; i++)
                        {
                            string assetPath = string.Empty;
                            for (int j = 0; j < files.Count; j++)
                            {
                                string[] splits = Path.GetFileName(files[j].ToLower()).Replace(".fbx", "").Split('@');
                                if (splits.Length >= 2)
                                {
                                    if (splits[1] == oacClips[i].Key.name.ToLower())
                                    {
                                        assetPath = files[j].ToLower();
                                        break;
                                    }
                                }
                                else
                                {
                                    if (splits[0] == oacClips[i].Key.name.ToLower())
                                    {
                                        assetPath = files[j].ToLower();
                                        break;
                                    }
                                }

                            }

                            if (!string.IsNullOrEmpty(assetPath))
                            {
                                KeyValuePair<AnimationClip, AnimationClip> pair = oacClips[i];
                                KeyValuePair<AnimationClip, AnimationClip> newPair;

                                AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(assetPath);
                                if (clip)
                                {
                                    AnimationEvent[] events = clip.events;
                                    foreach (AnimationEvent e in events)
                                    {
                                        if (e.functionName == XEditorAnimationClipTab.GetFunctionName(XAnimationEventName.Effect))
                                        {
                                            XEffectConfigObject effect = e.objectReferenceParameter as XEffectConfigObject;
                                            if (effect)
                                            {
                                                foreach (string effectPath in effect.effectFiles)
                                                {
                                                    bool added = true;
                                                    foreach (string old in eventEffects)
                                                    {
                                                        if (old == effectPath)
                                                        {
                                                            added = false;
                                                            break;
                                                        }
                                                    }

                                                    if (added)
                                                    {
                                                        eventEffects.Add(effectPath);
                                                    }
                                                }
                                            }
                       
                                        }
                                    }

                                    if (clip.name != oacClips[i].Key.name)
                                        clip.name = oacClips[i].Key.name;

                                    newPair = new KeyValuePair<AnimationClip, AnimationClip>(pair.Key, clip);
                                }
                                else
                                {
                                    newPair = new KeyValuePair<AnimationClip, AnimationClip>(pair.Key, pair.Value);
                                }

                                Debug.LogFormat("Add clip {0} {1}", newPair.Key, newPair.Value);
                                newClips.Add(newPair);
                            }
                        }

                        oac.ApplyOverrides(newClips);

                        string outName = Path.Combine(outPath, string.Format("{0}.{1}", name,
                            typeof(AnimatorOverrideController).Name.Replace(typeof(Animator).Name, "")));
                        GenerateAsset(outName, oac);

                        animator.runtimeAnimatorController = AssetDatabase.LoadAssetAtPath<AnimatorOverrideController>(outName);
                        animator.enabled = true;

                        if (srcfbx)
                        {
                            Animator srcAnimator = srcfbx.GetComponent<Animator>();
                            if (srcAnimator)
                            {
                                animator.avatar = srcAnimator.avatar;
                            }
                        }
                    }
                }
            }

            XActorPreload preload = prefab.GetComponent<XActorPreload>();
            if (!preload) {
                preload = prefab.AddComponent<XActorPreload>();
            }

            preload.effects = eventEffects.ToArray();
        }
    }

    public static void ExtractSkinMesh(GameObject prefab, GameObject srcfbx, string path)
    {
        List<string> files = GetAssetPathInFolder(
            Path.GetDirectoryName(path).Replace("\\", "/").Replace(XDirectory.Assets.ToString() + "/", ""), true, "*.mat");
        if (files.Count > 0)
        {
            List<Material> materials = new List<Material>();
            foreach (string file in files)
            {
                Material mat = AssetDatabase.LoadAssetAtPath<Material>(file);
                if (mat)
                    materials.Add(mat);
            }

            string subPath = Path.GetDirectoryName(path).Replace(XDirectory.Art.ToString(), XDirectory.Resources.ToString());
            string elementPath = Path.Combine(subPath, XDirectory.Element.ToString().ToLower());
            if (!Directory.Exists(elementPath))
                Directory.CreateDirectory(elementPath);

            string prefabPath = Path.Combine(subPath, XDirectory.Prefabs.ToString().ToLower());
            if (Directory.Exists(prefabPath))
                Directory.Delete(prefabPath, true);

            Directory.CreateDirectory(prefabPath);

            int boneWeightIndex = 0;
            SkinnedMeshRenderer[] renders = prefab.GetComponentsInChildren<SkinnedMeshRenderer>(true);

            for (int i = 0; i < renders.Length; i++)
            {
                SkinnedMeshRenderer smr = renders[i];
                if (smr)
                {
                    GameObject smrGo = GameObject.Instantiate(smr.gameObject);
                    if (!smrGo)
                        throw new System.NullReferenceException(smr.name);

                    XAvatarElement element = ScriptableObject.CreateInstance<XAvatarElement>();
                    if (element)
                    {
                        List<string> boneNames = new List<string>();
                        List<Matrix4x4>
                            bindPoses = new List<Matrix4x4>();

                        element.BoneWeights = new List<XBoneWeightRecord>();

                        foreach (Transform t in smr.bones)
                        {
                            boneNames.Add(t.name);
                            bindPoses.Add(t.worldToLocalMatrix);

                            element.BoneWeights.Add(new XBoneWeightRecord()
                            {
                                BoneName = t.name,
                                WeightIndex = boneWeightIndex
                            });

                            boneWeightIndex++;
                        }

                        element.SmrLocalToWorldMatrix = smr.transform.localToWorldMatrix;
                        element.BoneNames = boneNames;
                        element.BindPoses = bindPoses;

                        element.SharedMaterials = new List<Material>();

                        List<Material> shardMaterials = new List<Material>();

                        foreach (Material mat in materials)
                        {
                            if (mat.name.Contains(smr.name))
                            {
                                shardMaterials.Add(mat);
                            }
                        }

                        Object elementPrefab = GeneratePrefab(smrGo, prefabPath, smr.name);
                        element.Prefab = elementPrefab;
                        element.Name = smr.name;

                        if (shardMaterials.Count <= 0)
                            Debug.LogError("Can't find " + smr.name + " materials");

                        foreach (Material mat in shardMaterials)
                        {
                            XAvatarElement avatar = ScriptableObject.CreateInstance<XAvatarElement>();
                            avatar.Clone(element);

                            avatar.SharedMaterials = new List<Material>();
                            avatar.SharedMaterials.Add(mat);

                            string outElementConfigFullPath = Path.Combine(elementPath, mat.name + ".asset");

                            GenerateAsset(outElementConfigFullPath, avatar);
                        }

                        Object.DestroyImmediate(smrGo);
                    }
                }
            }
        }
    }
}

/// <summary>
/// 
/// </summary>
public class XEditorBuildAvatarTab : XEditorAvatarTab
{
    private Texture2D refreshTexture;

    /// <summary>
    /// 
    /// </summary>
    [System.Serializable]
    public class XAvatarTabConfigure
    {
        /// <summary>
        /// 
        /// </summary>
        public string inPath;

        /// <summary>
        /// 
        /// </summary>
        public string inObjectPath;

        /// <summary>
        /// 
        /// </summary>
        public List<string> onToggles = new List<string>();

        /// <summary>
        /// 
        /// </summary>
        public string outHeros;

        /// <summary>
        /// 
        /// </summary>
        public string outObject;
    }

    /// <summary>
    /// 
    /// </summary>
    [SerializeField]
    private XAvatarTabConfigure configure;

    /// <summary>
    /// 
    /// </summary>
    private List<XEditorToggleProperty> prefabToggles = new List<XEditorToggleProperty>();

    /// <summary>
    /// 
    /// </summary>
    private AnimatorController animatorControllerroller;

    /// <summary>
    /// 
    /// </summary>
    /// <param name="window"></param>
    public XEditorBuildAvatarTab(EditorWindow window)
    {
        prefabToggles = new List<XEditorToggleProperty>();
        configure = new XAvatarTabConfigure();
        configure.onToggles = new List<string>();
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="rect"></param>
    public void OnEnable(Rect rect)
    {
        string dataPath = XEditorUtils.MakeLibraryPath(typeof(XAvatarTabConfigure).Name);
        if (File.Exists(dataPath))
        {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open(dataPath, FileMode.Open);
            var data = bf.Deserialize(file) as XAvatarTabConfigure;
            if (data != null)
                configure = data;

            file.Close();
        }

        if (!string.IsNullOrEmpty(configure.inPath))
            OnPathChanged();

        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 
    /// </summary>
    public void OnDisable()
    {
        string dataPath = XEditorUtils.MakeLibraryPath(typeof(XAvatarTabConfigure).Name);

        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Create(dataPath);

        bf.Serialize(file, configure);
        file.Close();
    }

    /// <summary>
    /// 
    /// </summary>
    void OnPathChanged()
    {
        List<string> aryFiles = XEditorUtils.FindFolder(configure.inPath);
        if (aryFiles.Count > 0)
        {
            prefabToggles = new List<XEditorToggleProperty>();
            foreach (string file in aryFiles)
            {
                prefabToggles.Add(
                    new XEditorToggleProperty(false, file.Replace("\\", "/"), "", configure.onToggles)
                    );
            }
        }

        UpdateAnimatorController(configure.inPath);
    }

    /// <summary>
    /// 
    /// </summary>
    void OnAvatarPathGUI()
    {
        XEditorHelper.DrawTitle(typeof(XEditorAvatarBuilder).Name);
        XEditorHelper.BeginContents();

        EditorGUILayout.BeginHorizontal();
        configure.inPath = EditorGUILayout.TextField(typeof(Path).Name, configure.inPath);
        if (GUILayout.Button("...", GUILayout.Width(20)))
        {
            string newPath = EditorUtility.OpenFolderPanel(XDirectory.Prefab.ToString(), string.Empty, string.Empty);
            if (!string.IsNullOrEmpty(newPath))
            {
                newPath = newPath.Replace(Application.dataPath + "/", "");
                if (newPath != configure.inPath)
                {
                    configure.inPath = newPath;
                    OnPathChanged();
                }
            }
        }

        if (GUILayout.Button(refreshTexture, GUILayout.Width(20)))
        {
            OnPathChanged();
        }

        EditorGUILayout.EndHorizontal();
        EditorGUILayout.Space();
        EditorGUILayout.LabelField(typeof(AnimatorController).Name);
        animatorControllerroller = (AnimatorController)EditorGUILayout.ObjectField(animatorControllerroller, typeof(AnimatorController), false);
        XEditorHelper.EndContents();
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="path"></param>
    void UpdateAnimatorController(string path)
    {
        string animatorControllerPath;
        if (path.Contains(XDirectory.Humanoids.ToString()))
        {
            animatorControllerPath = Path.Combine(XDirectory.Assets.ToString(), XDirectory.Art.ToString(), XDirectory.AnimatorController.ToString(),
                ModelImporterAnimationType.Human.ToString());
        }
        else
        {
            animatorControllerPath = Path.Combine(XDirectory.Assets.ToString(), XDirectory.Art.ToString(), XDirectory.AnimatorController.ToString(),
                ModelImporterAnimationType.Generic.ToString());
        }

        if (!string.IsNullOrEmpty(animatorControllerPath))
        {
            animatorControllerroller = AssetDatabase.LoadAssetAtPath<AnimatorController>(Path.ChangeExtension(animatorControllerPath, "controller"));
        }
    }

    /// <summary>
    /// 
    /// </summary>
    void OnPrefabsGUI()
    {
        if (XEditorHelper.DrawHeader(XDirectory.Prefabs.ToString()))
        {
            XEditorHelper.BeginContents();
            for (int idx = 0; idx < prefabToggles.Count; idx++)
            {
                bool state = GUILayout.Toggle(prefabToggles[idx].state, Path.GetFileName(prefabToggles[idx].content.text));
                if (state != prefabToggles[idx].state)
                {
                    prefabToggles[idx].state = state;
                }
            }

            GUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("Build", GUILayout.MaxWidth(75f)))
                BuildHeros();
            if (GUILayout.Button("Reset", GUILayout.MaxWidth(75f)))
                BuildReset();
            GUILayout.EndHorizontal();

            EditorGUILayout.Space();
            XEditorHelper.EndContents();
        }
    }

    /// <summary>
    /// 
    /// </summary>
    void BuildHeros()
    {
        foreach(XEditorToggleProperty toggle in prefabToggles)
        {
            if (toggle.state)
            {
                BuildAvatar(toggle.content.text);
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    bool BuildAvatar(string path)
    {
        GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
        if (!prefab)
            return false;

        GameObject srcfbx = AssetDatabase.LoadAssetAtPath<GameObject>(Path.ChangeExtension(path, 
            XDirectory.Fbx.ToString().ToLower()));
        if (!srcfbx)
            return false;

        if (!animatorControllerroller)
        {
            EditorUtility.DisplayDialog("Tips", "Miss AnimationController", "ok", "cancel");
            return false;
        }

        XEditorAvatarBuilder.ExtractSkeleton(prefab, srcfbx, path, animatorControllerroller);
        XEditorAvatarBuilder.ExtractSkinMesh(prefab, srcfbx, path);
        
        return true;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="rect"></param>
    public void OnGUI(Rect rect)
    {
        if (!refreshTexture)
            refreshTexture = EditorGUIUtility.FindTexture("Refresh");

        OnAvatarPathGUI();
        OnPrefabsGUI();
    }

    /// <summary>
    /// 
    /// </summary>
    public void BuildReset()
    {
        OnPathChanged();
    }

    /// <summary>
    /// 
    /// </summary>
    public void OnReload()
    {
        OnPathChanged();
    }
}
